TDS sniffer와 decompile을 이용한 사용자 행동 분석

마지막 수정일 · 2026. 09. 13.

Problem

병원이라는 도메인에서 제일 어려웠던 건 기술이 아니라 사용자 조사였다.

20년 넘게 쓰인 치과 EMR을 대체하는 제품을 만들고 있는데, 대체하려는 대상이 기능이 아니라 그 20년치 사용 습관 전체였다. 그러면 당연히 "지금 어떻게 쓰고 계신지"를 먼저 알아야 flow를 짤 수 있는데, 이게 일반적인 유저 리서치 방식으로는 전혀 안 나왔다.

  • 연령대가 높은 사용자가 많아 설문·인터뷰 자체의 응답률이 낮고, 질문을 이해시키는 데에만 시간이 다 갔다
  • 한 프로그램에 너무 오래 적응한 사용자는 불편을 언어로 꺼내지 못한다. "뭐가 불편하세요"라고 물으면 대부분 "불편한 거 없다"고 답한다. 실제로 불편이 없는 게 아니라, 불편을 우회하는 습관이 이미 몸에 붙어서 불편으로 인식되지 않는 상태였다
  • 진료 중에 어깨너머로 관찰하는 것도 한계가 있다. 하루에 볼 수 있는 건 몇 케이스뿐이고, 관찰자가 옆에 있으면 평소처럼 쓰지 않는다

"사용자에게 물어보면 된다" 는 전제 자체가 이 도메인에서는 성립하지 않았다. 그렇다고 감으로 화면을 만들 수는 없었다. 여기서 방향을 하나 정했다.

물어서 안 나오면, 답은 사람이 아니라 프로그램에 남아 있다.

사용자는 본인의 사용 패턴을 설명하지 못하지만, 사용 패턴은 이미 20년 동안 프로그램 안에 흔적으로 쌓여 있다. 사람을 인터뷰하는 대신 프로그램을 인터뷰하기로 했다.



해결방안

관측 지점을 어디에 둘 것인가

기존 병원 EMR은 대부분 DB - 프론트 2-tier 구조다. 중간에 서버 로직이 거의 없고, 클라이언트가 MSSQL에 직접 붙어서 쿼리를 날린다. 이게 처음엔 분석하기 어려운 구조처럼 보였는데, 관측 관점에서는 오히려 기회였다. 중간 계층이 없다는 건 화면에서 일어난 일이 그대로 네트워크에 노출된다는 뜻이기 때문이다.

그래서 관측 축을 세 개로 나눴다.

code
[ 사람 ]  키보드 / 마우스 / 화면 전환   → Win32 hook + screen capture
[ 화면 ]  어떤 UI가 존재하는가          → decompile
[ 데이터 ] 그 동작이 무엇을 남기는가     → TDS sniffer

하나만으로는 의미가 없다. 쿼리만 보면 "무슨 일이 일어났는지"는 알아도 "왜 그렇게 했는지"를 모르고, 입력 로그만 보면 "어디를 눌렀는지"는 알아도 "그게 무슨 의미인지"를 모른다. 세 축을 같은 타임라인 위에 정렬하는 게 목표였다.



1. TDS sniffer — 데이터 축

처음엔 Wireshark로 시작했다. 그런데 실사용에 들어가니 두 가지가 걸렸다.

  • BPF 필터가 생각만큼 잘 걸리지 않고, 걸러도 쿼리를 사람이 읽을 수 있는 형태로 보기가 힘들다
  • 긴 쿼리와 sp_executesql RPC 파라미터가 여러 패킷에 걸쳐 쪼개진다. 한 패킷만 보면 쿼리가 잘려 있어서 화면 동작과 짝지을 수가 없다

Wireshark도 결국 npcap으로 패킷을 가져오는 구조라서, 같은 소스에서 우리가 필요한 형태로 직접 뽑아내면 된다고 판단했다. 그래서 Rust로 TDS 스니퍼를 만들었다.
tds-sniffer

레이어로 보면 우리가 손대야 하는 지점은 명확했다.

code
[ 7 Application  ]  TDS (SQL, Result)   ← 최종 목표
[ 6 Presentation ]  TLS / Encoding
[ 5 Session      ]  DB Session
[ 4 Transport    ]  TCP                 ← 재조립이 핵심
[ 3 Network      ]  IP
[ 2 Data Link    ]  Ethernet
[ 1 Physical     ]  Wire / Fiber

처리 파이프라인은 이렇게 구성했다.

code
npcap raw bytes
   ↓
[1] L2/L3 파싱 (Ethernet / IPv4)      · extractor.rs — zero-copy
   ↓
[2] TCP flow 식별 / 정규화             · tcp.rs[3] TCP stream 재조립                  · tcp.rs      ← 핵심
   ↓
[4] TDS message framing                · tds.rs[5] SQLBatch / RPC 디코딩              · tds.rs[6] table · operation 분류 후 이벤트화  · output.rs[7] 중복 제거 + 인덱싱                  · store.rs[8] egui GUI 필터링 / 로그 파일         · gui.rs, logging.rs

만들면서 알게 된 건 진짜 난이도는 프로토콜 파싱이 아니라 [3] TCP 재조립에 있다는 점이었다. TDS 프레임은 TCP 세그먼트 경계와 전혀 맞지 않아서, 세그먼트 단위로 파싱하면 긴 쿼리는 항상 중간에서 잘린다. 여기를 제대로 처리하고 나서야 비로소 "버튼 하나 누를 때 나가는 쿼리 N개"를 온전한 형태로 볼 수 있었다.

로그는 두 벌로 남기게 했다. 사람이 바로 읽는 용도의 SQL 텍스트(log/basic/)와, 파싱이 틀렸을 때 원인을 되짚기 위한 raw hex(log/raw/)다. 분석 도구를 만들 때 도구 자신이 틀렸을 가능성을 검증할 수 있게 해두는 것은 생각보다 중요했다. 실제로 초기에 "이상한 쿼리"로 보였던 것 중 상당수가 우리 파서의 버그였다.



2. Decompile — 화면 축

쿼리만으로는 "이 쿼리가 어느 화면에서 나갔는지"를 알 수 없다. 그래서 기존 프로그램을 디컴파일해서 UI 쪽 구조를 확보했다. 결과물은 C# 파일 4,332개였다.

사람이 다 읽는 건 불가능한 양이었고, 여기서부터가 AI를 제대로 써야 하는 구간이었다. 다만 그냥 "이 코드베이스 분석해줘"로 던지면 답이 그럴듯하게 나오고 틀린다. 이 분석 결과 위에 제품 flow를 얹을 거라서, 틀린 근거는 아예 없는 것보다 나빴다.

그래서 모델을 믿는 대신 방법 자체에 제약을 걸었다.

  • 파일 간 참조/호출 관계를 먼저 지식 그래프로 만들어두고, 에이전트가 "전체를 읽고 요약"하는 게 아니라 그래프를 따라 탐색하게 했다
  • 모든 주장에 원문 라인 인용을 강제했다. 인용을 못 붙이는 결론은 결론으로 인정하지 않았다
  • 화면 단위로 질문을 쪼개서, "이 폼에서 저장 버튼을 누르면 어떤 테이블이 건드려지는가" 같은 검증 가능한 형태로만 물었다

환각을 모델 성능으로 막으려 하지 않고 방법 규칙으로 막았다는 게 핵심이다. 인용 라인이 붙어 있으면 내가 그 라인만 열어서 5초 만에 검증할 수 있다. 검증 비용이 낮아지니까 에이전트에게 훨씬 과감하게 일을 맡길 수 있었고, 결과적으로 사람이 직접 읽는 것보다 훨씬 넓은 범위를 커버했다.

그리고 여기서 나온 UI 구조와 sniffer에서 나온 쿼리를 짝지었다. 화면 → 쿼리 매핑이 만들어지면, 반대로 쿼리 로그만 보고 사용자가 어떤 화면을 어떤 순서로 거쳤는지 복원할 수 있게 된다.



3. Win32 hook + 화면 capture — 사람 축

마지막으로 실제 사용 장면이 필요했다. Win32 훅으로 키보드·마우스 입력과 포커스/화면 전환을 기록하고, 화면 캡처를 함께 남겼다.

이 축이 왜 필요했냐면, 앞의 두 축은 "무엇을 했는가"까지만 알려주기 때문이다. 사람 축이 붙어야 아래 같은 게 보인다.

  • 같은 결과에 도달하는데 경로가 사람마다 다르다 (단축키 vs 메뉴 vs 다른 화면 경유)
  • 어떤 화면에서 입력이 멈춰 있는 시간이 유독 길다 → 여기가 실제 병목
  • 마우스가 특정 영역을 왕복한다 → 정보가 두 곳에 나뉘어 있다는 신호
  • 같은 값을 매일 똑같이 입력한다 → 자동화 대상

특히 마지막 항목은 예상 못 한 수확이었다. 전문 병원일수록 매일 같은 치료·검진이 반복되는데, 매번 같은 차팅을 손으로 하고 있었다. 이건 인터뷰로는 절대 안 나온다. 본인들에게는 그게 "일"이지 "반복"이 아니기 때문이다.

세 축을 타임스탬프로 정렬하면 이런 형태가 된다.

code
t0   [입력]  F2 키
t0+  [화면]  환자 검색 폼 포커스
t1   [쿼리]  SELECT ... FROM PATIENT WHERE ...
t2   [입력]  마우스 클릭 (그리드 3행)
t3   [쿼리]  SELECT ... FROM CHART WHERE PT_ID = ...
t3+  [화면]  차트 화면 전환
...

여기까지 오면 더 이상 "의사들이 어떻게 쓰는지 모르겠다"가 아니라, 재생 가능한 시나리오 데이터가 된다.



4. 관측을 테스트로 바꾸기

분석 자료로만 쓰고 끝내면 아까웠다. 위에서 만든 시나리오는 그대로 테스트 케이스의 정의였다.

  • 관측된 실제 사용 흐름을 e2e 시나리오로 옮겼다. 우리가 상상한 happy path가 아니라 실제로 사람이 밟는 경로가 기준이 된다
  • 기존 프로그램이 특정 동작에서 남기는 쿼리/데이터 결과를 기대값(oracle) 으로 삼았다. 같은 입력에 대해 우리 제품이 같은 데이터 상태를 만드는지 비교할 수 있다
  • 제품이 위젯 / dock 단위로 쪼개져 있어서 테스트 경계가 명확했고, laboratory 개념을 만들어 에이전트가 직접 배치를 바꿔가며 e2e를 돌리는 구조까지 확장했다

"기존 프로그램과 결과가 같은가"를 자동으로 비교할 수 있게 된 게 제일 컸다. 마이그레이션 제품에서 정답지는 결국 기존 시스템인데, 그 정답지를 사람 확인이 아니라 데이터로 확보한 셈이다.



성과

가장 직접적인 성과는, 우리 자체 테스트를 전부 통과하던 결함 5건을 이 경로로만 찾아냈다는 것이다.

결함 유형 우리 테스트에서 안 잡힌 이유
잘못된 바이트를 전송 화면상으로는 정상 저장. 실제 나가는 값이 다름
정상 업무를 막음 우리가 상상한 flow에는 없는 경로. 실사용에서만 밟힘
비정상을 통과시킴 기존 프로그램은 거부하는 입력을 우리는 수용

방향이 셋 다 달랐다는 게 중요하다. 한 종류의 버그가 아니라, "우리가 무엇을 모르는지 몰랐던" 영역이 통째로 드러난 것이다. 이건 기능 테스트를 아무리 촘촘히 짜도 나오지 않는다. 기준 자체를 우리가 만들고 있었기 때문이다.

이 근거 위에서 청구·EDI 검증 136/139, HIRA 사유코드 1,299개를 이관했다. 관측 데이터가 없었으면 "이 검증 규칙이 실제로 쓰이긴 하는가"를 판단할 방법이 없어서, 전수 이관 아니면 감으로 취사선택이었을 것이다.

부수적으로 제품 방향 두 개가 여기서 나왔다.

  • 병원마다 사용 패턴이 크게 다르다 → 개별 커스터마이징 대응 대신 위젯 도킹 시스템으로 흡수
  • 반복 업무 비중이 높다 → 단순 Q&A 챗봇이 아니라 실제로 화면을 조작하는 에이전트

둘 다 "그런 요청이 들어와서" 만든 게 아니라, 관측 데이터에서 먼저 발견하고 만든 기능이다.



회고

이번 건에서 개인적으로 남은 건 두 가지다.

첫째, 측정할 수 없으면 측정 도구부터 만든다. Wireshark가 안 되니까 포기하거나 감으로 가는 대신 스니퍼를 만든 게 이 프로젝트 전체의 출발점이었다. 도구 만드는 데 쓴 시간보다, 그 도구가 없어서 잘못 만들었을 화면을 되돌리는 비용이 훨씬 컸을 것이다.

둘째, AI에게 일을 맡길 때는 결과가 아니라 방법에 제약을 건다. 4,332개 파일을 사람이 읽는 건 불가능하고, 그렇다고 요약을 그대로 믿을 수도 없다. 원문 인용 강제 + 지식 그래프 탐색이라는 규칙을 먼저 세워두니, 결과를 신뢰할 수 있는 게 아니라 결과를 싸게 검증할 수 있게 됐다. 신뢰가 아니라 검증 비용이 핵심이라는 걸 이 과정에서 배웠다.



필요 개선사항

  1. TLS 환경 대응
    현재 스니퍼는 평문 TDS를 전제로 한다. 암호화된 구간에서는 [6] Presentation 레이어 복호화가 필요한데, 지금 구조에서는 아예 관측이 불가능하다. 환경에 따라 이 방법 자체가 막힐 수 있다는 게 가장 큰 한계다.
  2. 화면 ↔ 쿼리 자동 correlation
    지금은 타임스탬프 기준 정렬 + 수동 매핑이 섞여 있다. 화면 전환 이벤트와 쿼리 burst를 자동으로 묶는 수준까지 올리면, 시나리오 추출이 사람 손을 거의 안 타게 된다.
  3. 캡처 데이터 비식별화
    화면 캡처와 쿼리에는 환자 정보가 그대로 들어간다. 수집 단계에서 마스킹하는 파이프라인이 지금은 충분하지 않아서, 운영 환경으로 확대하기 전에 반드시 정리해야 한다.
  4. e2e 커버리지 측정
    관측된 시나리오 중 몇 %가 실제 테스트로 옮겨졌는지 세고 있지 않다. "실사용 경로 기준 커버리지"라는 지표를 만들면 테스트의 의미가 훨씬 명확해질 것 같다.